package org.exussum0.eyecks1;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;

public class Driver {

	@Root
	public static class Config {
		@ElementList(required = false)
		private List<String> srcClassExcludePatterns = new ArrayList();
		@ElementList(required = false)
		private List<String> destClassExcludePatterns = new ArrayList();
		@ElementList(required = false)
		private List<String> packages = new ArrayList();
		@Element
		private String outputFile;
		@ElementList(required = false)
		private List<String> clusters = new ArrayList();
		@ElementList
		private List<String> directories = new ArrayList();
		@Element(required = false)
		private boolean clusterSrc = true;
		@Element(required = false)
		private boolean clusterDest = true;

		public List<String> getSrcClassExcludePatterns() {
			return srcClassExcludePatterns;
		}

		public void setSrcClassExcludePatterns(
				List<String> srcClassExcludePatterns) {
			this.srcClassExcludePatterns = srcClassExcludePatterns;
		}

		public List<String> getDestClassExcludePatterns() {
			return destClassExcludePatterns;
		}

		public void setDestClassExcludePatterns(
				List<String> destClassExcludePatterns) {
			this.destClassExcludePatterns = destClassExcludePatterns;
		}

		public List<String> getPackages() {
			return packages;
		}

		public void setPackages(List<String> packages) {
			this.packages = packages;
		}

		public String getOutputFile() {
			return outputFile;
		}

		public void setOutputFile(String outputFile) {
			this.outputFile = outputFile;
		}

		public List<String> getClusters() {
			return clusters;
		}

		public void setClusters(List<String> clusters) {
			this.clusters = clusters;
		}

		public List<String> getDirectories() {
			return directories;
		}

		public void setDirectories(List<String> directories) {
			this.directories = directories;
		}

		public boolean isClusterSrc() {
			return clusterSrc;
		}

		public void setClusterSrc(boolean clusterSrc) {
			this.clusterSrc = clusterSrc;
		}

		public boolean isClusterDest() {
			return clusterDest;
		}

		public void setClusterDest(boolean clusterDest) {
			this.clusterDest = clusterDest;
		}
	}

	public static void main(String args[]) throws Exception {	
		if (args.length == 0) {
			System.err
					.println("Usage: org.exussum0.e-yecks1.Driver <file1> <file2> ... <fileN>");
			System.exit(1);
		}

		ClassResolver classResolver = new ClassResolver();
		Serializer serializer = new Persister();

		for (String config : args) {
			Config configXml = serializer.read(Config.class, new File(config));

			HashSet<Pattern> srcExcludePatterns = new HashSet<Pattern>();
			for (String pattern : configXml.srcClassExcludePatterns)
				srcExcludePatterns.add(Pattern.compile(pattern));
			HashSet<Pattern> destExcludePatterns = new HashSet<Pattern>();
			for (String pattern : configXml.destClassExcludePatterns)
				destExcludePatterns.add(Pattern.compile(pattern));

			Driver driver = new Driver();
			driver.setClassResolver(classResolver);
			driver.setIncludedPackages(new HashSet<String>(configXml.packages));
			driver.setSrcExcludes(srcExcludePatterns);
			driver.setDestExcludes(destExcludePatterns);
			driver.setOutputFile(configXml.outputFile);
			driver.setClusters(configXml.clusters);
			driver.setLinkClusterLeft(configXml.clusterSrc);
			driver.setLinkClusterRight(configXml.clusterDest);

			Map<String, Set<String>> dependencies = driver
					.getDependencies(configXml.directories);

			Set<String> packages = new HashSet<String>();
			Set<String> classes = new HashSet<String>();
			for (String src : dependencies.keySet()) {
				packages.add(classResolver.getPackage(src));
				classes.add(src);
				for (String dest : dependencies.get(src)) {
					packages.add(classResolver.getPackage(dest));
					classes.add(dest);
				}
			}

			Map<String, String> packageColours = driver
					.correlateColours(packages);
			driver.write(dependencies, packages, classes, packageColours);
		}
	}

	private Set<String> includedPackages = Collections.emptySet();
	private Set<Pattern> srcExcludes = Collections.emptySet();
	private Set<Pattern> destExcludes = Collections.emptySet();

	private String outputFile;
	private List<String> clusters;
	private ClassResolver classResolver;
	private boolean linkClusterLeft;

	private boolean linkClusterRight;

	private Map<String, String> correlateColours(Set<String> strings) {
		Map<String, String> result = new HashMap<String, String>();

		int step = 2 * strings.size() / 3;
		if (step == 0)
			step = 1;
		step = (int) Math.floor(128 / step);
		if (step == 0)
			step = 1;

		List<String> colours = new ArrayList<String>();

		for (int red = 128; red < 256; red += step)
			for (int green = 128; green < 256; green += step)
				for (int blue = 128; blue < 256; blue += step) {
					if (red + blue + green > 640)
						continue;
					colours.add(toHex(red) + toHex(green) + toHex(blue));
				}

		Collections.shuffle(colours);
		for (String pkg : strings) {
			result.put(pkg, colours.remove(0));
		}
		return result;
	}

	private String formatClass(String clazz) {
		return clazz.replaceAll("\\.", ".\\\\n");
	}

	public Map<String, Set<String>> getDependencies(List<String> directories)
			throws Exception {

		classResolver.setIncludes(includedPackages);

		Map<String, Set<String>> uses = new HashMap<String, Set<String>>();

		DependencyAccumulator accumulator = new DependencyAccumulator();
		accumulator.setClassFinder(new ClassFinder());
		accumulator.setClassResolver(classResolver);
		accumulator.setUses(uses);
		accumulator.setLeftExcludes(srcExcludes);
		accumulator.setRightExcludes(destExcludes);

		FsTraverser traverser = new FsTraverser();

		for (String dir : directories)
			traverser.traverse(new File(dir), accumulator);
		return uses;
	}

	private String resolveCluster(boolean linkCluster, List<String> clusters,
			String clazz, String clazzFormatted) {
		if (linkCluster) {
			for (String cluster : clusters) {
				if (clazz.startsWith(cluster))
					return "cluster_" + cluster.replaceAll("\\.", "_");
			}
		}
		return clazzFormatted;
	}

	public void setClassResolver(ClassResolver classResolver) {
		this.classResolver = classResolver;
	}

	private void setClusters(List<String> list) {
		this.clusters = list;
	}

	public void setDestExcludes(Set<Pattern> destExcludes) {
		this.destExcludes = destExcludes;
	}

	public void setIncludedPackages(Set<String> includedPackages) {
		this.includedPackages = includedPackages;
	}

	public void setLinkClusterLeft(boolean linkClusterLeft) {
		this.linkClusterLeft = linkClusterLeft;
	}

	public void setLinkClusterRight(boolean linkClusterRight) {
		this.linkClusterRight = linkClusterRight;
	}

	private void setOutputFile(String outputFile) {
		this.outputFile = outputFile;
	}

	public void setSrcExcludes(Set<Pattern> srcExcludes) {
		this.srcExcludes = srcExcludes;
	}

	private String toHex(int val) {
		String result = Integer.toHexString(val);
		if (result.length() == 1)
			result = "0" + result;
		return result;
	}

	private void write(Map<String, Set<String>> dependencies,
			Set<String> packages, Set<String> classes,
			Map<String, String> packageColours) throws IOException {
		PrintStream output = new PrintStream(new File(outputFile));

		// header
		output
				.println("digraph abstract { compound=true;\nranksep=2;node[shape=box,style=filled];");

		// nodes
		for (String cluster : clusters) {
			output.println("subgraph cluster_" + cluster.replaceAll("\\.", "_")
					+ " {");
			output.println("label=\"" + cluster + "\";color=blue;");
			for (Iterator<String> i = classes.iterator(); i.hasNext();) {
				String clazz = i.next();
				if (clazz.startsWith(cluster)) {
					i.remove();
					String pkg = classResolver.getPackage(clazz);
					output.println("node [color=\"#" + packageColours.get(pkg)
							+ "\"] \"" + formatClass(clazz) + "\" ;");
				}
			}
			output.println("}");
		}
		for (String clazz : classes) {
			String pkg = classResolver.getPackage(clazz);
			output.println("node [color=\"#" + packageColours.get(pkg)
					+ "\"] \"" + formatClass(clazz) + "\" ;");
		}

		// relationships
		for (String src : dependencies.keySet()) {
			String srcFormatted = formatClass(src);
			String srcTail = resolveCluster(linkClusterLeft, clusters, src,
					srcFormatted);

			for (String dest : dependencies.get(src)) {
				String destFormatted = formatClass(dest);
				String destTail = resolveCluster(linkClusterRight, clusters,
						dest, destFormatted);
				output.println('"' + srcFormatted + "\" -> \"" + destFormatted
						+ "\" [ltail=\"" + srcTail + "\", lhead=\"" + destTail
						+ "\"];");
			}
		}
		output.println("}");
		output.close();
	}
}
